Pro ASP.NET Core MVC2(第7版)翻译

第26章:模型绑定

作者:Adam Freeman
翻译:陈广
日期:2018-10-20


模型绑定是使用来自 HTTP 请求的数据创建 .NET 对象的过程,以便为 action 方法提供所需的参数。在本章中,我将描述模型绑定系统的工作方式;演示如何绑定简单类型、复杂类型和集合;并演示如何控制流程以指定请求的哪一部分提供 action 方法所需的数据值。表26-1为模型绑定简历。

表 26-1:模型绑定简历

问题 回答
它们是什么? 模型绑定是使用从 HTTP 请求获得的数据值创建 action 方法所需的对象作为参数的过程。
它们有何用途? 模型绑定允许 action 方法使用 c# 类型声明参数,并自动接收来自请求的数据,而不必检查、解析和直接处理数据。
如何使用它们? action 方法以最简单的形式声明参数,其名称用于从 HTTP 请求检索数据值。可以通过将属性应用到 action 方法参数来配置用于获取数据的请求的部分。
是否有任何缺陷或限制? 主要的缺陷是从请求的错误部分获取数据。我在《理解模型绑定》一节中解释了如何搜索请求数据,并且可以使用我在《指定模型绑定源》一节中描述的属性显式地指定搜索位置。
有没有其他选择? action 方法根本不必声明参数,可以使用我在第17章中描述的 context 对象直接从 HTTP 请求获取数据。然而,结果是更复杂的代码,很难阅读和维护。

表26-2为本章摘要

表 26-2:本章摘要

问题 解决方案 清单
绑定到简单类型或集合 向 action 方法添加参数 1-10,23-29
绑定到复杂类型 确保视图生成的 HTML 结构良好 11-19
有选择地绑定属性 使用Bind特性指定数据值的名称,或使用BindNever特性将模型属性排除在绑定过程中。 20-22
指定数据绑定值的源 将属性应用于 action 方法参数或模型属性,该属性标识绑定值应来自何处 30-38

准备示例项目

在本章中,我使用【ASP.NET Core Web 应用程序(.net core)】模板创建了一个名为 MvcModels 的新的空项目。

创建模型和存储库

我创建了 Models 文件夹,并添加了一个名为 Person.cs 的类文件,用于定义清单26-1所示的类和枚举。

清单 26-1:Models 文件夹下的 Person.cs 文件的内容

using System;

namespace MvcModels.Models
{
    public class Person
    {
        public int PersonId { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public DateTime BirthDate { get; set; }
        public Address HomeAddress { get; set; }
        public bool IsApproved { get; set; }
        public Role Role { get; set; }
    }

    public class Address
    {
        public string Line1 { get; set; }
        public string Line2 { get; set; }
        public string City { get; set; }
        public string PostalCode { get; set; }
        public string Country { get; set; }
    }

    public enum Role
    {
        Admin,
        User,
        Guest
    }
}

接下来,我在 Models 文件夹中添加了一个名为 Repository.cs 的类文件,并定义了如清单26-2所示的接口和实现类。

清单 26-2:Models 文件夹下的 Repository.cs 文件的内容

using System.Collections.Generic;

namespace MvcModels.Models
{
    public interface IRepository
    {
        IEnumerable<Person> People { get; }
        Person this[int id] { get; set; }
    }
    public class MemoryRepository : IRepository
    {
        private Dictionary<int, Person> people
            = new Dictionary<int, Person>
        {
            [1] = new Person
            {
                PersonId = 1,
                FirstName = "Bob",
                LastName = "Smith",
                Role = Role.Admin
            },
            [2] = new Person
            {
                PersonId = 2,
                FirstName = "Anne",
                LastName = "Douglas",
                Role = Role.User
            },
            [3] = new Person
            {
                PersonId = 3,
                FirstName = "Joe",
                LastName = "Able",
                Role = Role.User
            },
            [4] = new Person
            {
                PersonId = 4,
                FirstName = "Mary",
                LastName = "Peters",
                Role = Role.Guest
            }
        };

        public IEnumerable<Person> People => people.Values;

        public Person this[int id]
        {
            get
            {
                return people.ContainsKey(id) ? people[id] : null;
            }
            set
            {
                people[id] = value;
            }
        }
    }
}

IRepository接口定义了一个People属性来检索模型中的所有对象,并定义了一个索引器,该索引器允许检索或存储单个Person对象。MemoryRepository类使用具有某些默认内容的字典实现接口。存储库实现不是持久的,因此当应用程序停止或重新启动时,应用程序的状态将恢复为默认内容。

创建控制器和视图

我创建了 Controllers 文件夹,添加了一个名为 HomeController.cs 的类文件,并使用它定义了如清单26-3所示的控制器。控制器通过依赖注入来接收存储库,它在Index方法中使用它的PersonId属性值从存储库中选择单个Person对象。

清单 26-3:Controllers 文件夹下的 HomeController.cs 文件的内容

using Microsoft.AspNetCore.Mvc;
using MvcModels.Models;

namespace MvcModels.Controllers
{
    public class HomeController : Controller
    {
        private IRepository repository;

        public HomeController(IRepository repo)
        {
            repository = repo;
        }

        public ViewResult Index(int id) => View(repository[id]);
    }
}

为了提供一个视图的 action 方法,我创建了 Views/Home 文件夹,并添加了一个名为 Index.cshtml 的 Razor 文件,其标记如清单26-4所示,它显示了表中模型对象的一些属性。

清单 26-4:Views/Home 文件夹下的 Index.cshtml 文件的内容

@model Person
@{ Layout = "_Layout"; }

<div class="bg-primary m-1 p-1 text-white"><h2>Person</h2></div>

<table class="table table-sm table-bordered table-striped">
    <tr><th>PersonId:</th><td>@Model.PersonId</td></tr>
    <tr><th>First Name:</th><td>@Model.FirstName</td></tr>
    <tr><th>Last Name:</th><td>@Model.LastName</td></tr>
    <tr><th>Role:</th><td>@Model.Role</td></tr>
</table>

Index.cshtml 视图依赖于共享布局,我创建了 Views/Shared 文件夹,并向其添加了一个名为 _Layout.cshtml 的布局,其内容见清单26-5。 清单 26-5: Views/Shared 文件夹下的 _Layout.cshtml 文件

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>@ViewBag.Title</title>
    <link asp-href-include="/lib/twitter-bootstrap/**/*.min.css" rel="stylesheet" />
    @RenderSection("scripts", false)
</head>
<body class="m-1 p-1">
    @RenderBody()
</body>
</html>

布局包括 bootstrap 样式表的一个link元素,并呈现视图的内容。还有一个可选的scripts section,我将在本章后面使用。为了简化本章中使用的视图,我将包含模型类的命名空间添加到视图文件夹中的 _ViewImports.cshtml 文件中,如清单26-6所示。

清单 26-6:Views 文件夹下的 _ViewImports.cshtml 文件,导入命名空间

@using MvcModels.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

视图依赖于 Bootstrap CSS 框架,为了将 Bootstrap 添加到示例项目中,我在 MvcModels 项目中单击鼠标右键,在弹出菜单中选择【添加】➤【添加客户端库】,并将 twitter-bootstrap 添加至项目中。最终生成的 libman.json 配置文件代码清单26-7所示:

清单 26-7:MvcModels 文件夹下的 libman.json 文件,添加包

{
  "version": "1.0",
  "defaultProvider": "cdnjs",
  "libraries": [
    {
      "library": "twitter-bootstrap@4.1.3",
      "destination": "wwwroot/lib/twitter-bootstrap/"
    }
  ]
}

配置应用程序

为了完成示例应用程序的初始设置,我在Startup类中启用了 MVC 框架和其他对的开发有用的中间件,如清单26-8所示。我还为存储库创建了一个服务,以便控制器能够访问数据模型。

清单 26-8:MvcModels 文件夹下的 Startup.cs 文件的内容

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using MvcModels.Models;

namespace MvcModels
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<IRepository, MemoryRepository>();
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStatusCodePages();
            app.UseDeveloperExceptionPage();
            app.UseStaticFiles();
            app.UseMvc(routes => {
                routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

启动应用程序并请求 /Home/Index/1 URL,生成如图26-1所示的结果(当前默认的 URL 将产生一个错误)。

图26-1 运行示例应用程序

理解模型绑定

模型绑定是 HTTP 请求和 C# action 方法之间的一座优雅的桥梁。大多数 MVC 应用程序在某种程度上依赖于模型绑定,包括本章的示例应用程序。在上一节中测试示例应用程序时使用了模型绑定。我请求的 URL 包含了我想查看的Person对象的PersonId属性的值,如下所示:

/Home/Index/1

MVC 转化了 URL 的这一部分,并在 Home 控制器类中调用Index方法为请求提供服务时使用它作为参数。

...
public ViewResult Index(int id) => View(repository[id]);
...

为了能够调用Index方法,MVC 需要id参数的值,提供该值是模型绑定系统的责任,它负责提供可用于调用 action 方法的数据值。

模型绑定系统依赖于模型绑定,它是负责从请求或应用程序的某个部分提供数据值的组件。默认模型绑定器在以下三个位置查找数据值:

  • 表单数据值
  • 路由变量
  • 查询字符串

每个数据源都会按顺序被检查,直到找到参数的值为止。在示例应用程序中没有表单数据,所以在那里找不到值。但是清单26-8中使用的应用程序配置中有一个名为id的路由段,它允许模型绑定系统为 MVC 提供一个可用于调用Index方法的值。搜索在找到合适的数据值之后停止,这意味着通过查询字符串不会搜索到数据值。

提示:在《指定模型绑定源》一节中,我将解释如何使用属性指定模型绑定数据的来源。这允许您从查询字符串获得指定数据值,即使表单或路由中也有合适的数据。

知道查找数据值的顺序很重要,因为请求可以包含多个值,如下面的 URL:

/Home/Index/3?id=1

路由系统将处理请求,并将 URL 模板中的id段与值3匹配,查询字符串包含id1。由于在查询字符串之前搜索路由数据,Index action 方法将接收值3,而查询字符串值将被忽略。

另一方面,如果您请求一个没有id段的 URL,那么将检查查询字符串,这意味着这样的 URL 还将允许模型绑定系统为id参数提供一个值,以便它能够调用Index方法:

/Home/Index?id=1

您可以在图26-2中看到这两个 URL 的效果。

图26-2 模型绑定数据源排序的效果

理解默认绑定值

模型绑定是一种尽力而为的特性,这意味着 MVC 将使用模型绑定来获取调用 action 方法所需的值,但即使无法提供数据值,也仍然会调用该方法。这可能会导致一些意想不到的行为,例如,请求 URL /Home/Index 会产生如图26-3所示的异常。

图26-3 处理模型属性的错误

这个异常不是由模型绑定系统报告的,而是在处理由Index action 方法选择的 Index 视图时发生的。为了调用Index方法,MVC 必须为id参数提供一个值,因此它要求每个模型绑定者检查其请求部分并提供一个值。

示例中没有表单数据,id路由段没有值,URL 中也没有查询字符串,这意味着模型绑定系统无法提供数据值。为了调用Index方法,MVC 必须为id参数提供一些值,因此它使用了一个默认值,并希望得到最好的结果。对于int参数,默认值是0,这就是导致异常的原因。Index方法的定义使用id参数的值从存储库检索模型对象。

...
public ViewResult Index(int id) => View(repository[id]);
...

当 MVC 使用默认值时,action 方法尝试检索id0的数据模型对象。没有这样的对象,存储库返回null,然后将其传递给控制器的View方法,以便向 Index.cshtml 视图提供视图模型数据。当 Index.cshtml 文件中的 Razor 表达式试图访问视图模型对象的属性时,它们会导致如图26-3所示的NullReferenceException

这意味着必须编写 action 方法来处理模型绑定系统提供的默认值,这可以通过多种方式完成。您可以向路由 URL 模式添加默认值(如第15章所述),为 action 方法参数分配默认值,或确保 action 方法不会作为其响应的一部分传递坏的数据值。最佳方法将取决于 action 方法正在做什么;在清单26-9中,我采用了最后一种方法,即修改 action 方法,以便确保Person对象始终传递给View方法,即使id参数与数据模型中的对象不对应。

清单 26-9:Controllers 文件夹下的 HomeController.cs 文件,防范默认值

using Microsoft.AspNetCore.Mvc;
using MvcModels.Models;
using System.Linq;

namespace MvcModels.Controllers
{
    public class HomeController : Controller
    {
        private IRepository repository;

        public HomeController(IRepository repo)
        {
            repository = repo;
        }

        public ViewResult Index(int id) =>
            View(repository[id] ?? repository.People.First());
    }
}

action 方法使用 LINQ 和空合并操作符在id参数的值不检索对象时返回存储库中的第一个对象。

绑定简单类型

如果有合适的值可用,必须将其转换为 C# 值,以便可以使用它来调用 action 方法。简单类型是从请求中可以从字符串中解析的一项数据中产生的值。这包括数值、bool值、日期,当然还有字符串值。

Index action 方法的id参数是int,因此模型绑定过程通过将id段变量解析为int值为 MVC 提供了一个值。

如果请求值无法转换(例如,如果我为一个需要int值的参数提供了apple值),那么模型绑定过程将无法为应用程序提供一个值,并且将使用默认值。

这带来了一个问题,因为这意味着在两种情况下,action 方法将接收默认值 0。第一个是当请求包含一个不能解析为参数类型的值时,例如对于URL /Home/Index/Apple;第二个是当请求包含一个可以解析的值,并且它恰好是零时,例如对于 URL /Home/Index/0。

大多数应用程序需要能够区分这些情况,最简单的方法是为 action 方法参数使用可空类型,如清单26-10所示。

清单 26-10:Controllers 文件夹下的 HomeController.cs 文件,使用可空类型

using Microsoft.AspNetCore.Mvc;
using MvcModels.Models;
using System.Linq;

namespace MvcModels.Controllers
{
    public class HomeController : Controller
    {
        private IRepository repository;

        public HomeController(IRepository repo)
        {
            repository = repo;
        }

        public IActionResult Index(int? id)
        {
            Person person;
            if (id.HasValue && (person = repository[id.Value]) != null)
            {
                return View(person);
            }
            else
            {
                return NotFound();
            }
        }
    }
}

可空类型的默认值为null,这允许我区分两种请求,分别为不包含可以解析为int的值的请求,以及可以解析为int值但int值恰好为零的请求。本例中Index方法的实现使用NotFound方法返回一个 404 错误,如果可空参数没有值,或者该值不对应于模型中的对象,这比简单地希望模型中的第一个对象合适更为健壮,这是我在上一节中采取的方法。

绑定复杂类型

当 action 方法参数是复杂类型时(也就是不能从单个字符串值解析的任何类型),模型绑定过程将使用反射获取目标类型的公共属性集,并依次对每个属性执行绑定过程。为了演示这是如何工作的,我向 Home 控制器添加了两个 action 方法,如清单26-11所示。

清单 26-11:Controllers 文件夹下的 HomeController.cs 文件,添加 Action 方法

using Microsoft.AspNetCore.Mvc;
using MvcModels.Models;
using System.Linq;

namespace MvcModels.Controllers
{
    public class HomeController : Controller
    {
        private IRepository repository;

        public HomeController(IRepository repo)
        {
            repository = repo;
        }

        public IActionResult Index(int? id)
        {
            Person person;
            if (id.HasValue && (person = repository[id.Value]) != null)
            {
                return View(person);
            }
            else
            {
                return NotFound();
            }
        }

        public ViewResult Create() => View(new Person());

        [HttpPost]
        public ViewResult Create(Person model) => View("Index", model);
    }
}

不带参数的Create方法的版本创建了一个新的Person对象并将其传递给View方法,它的效果是选择与 action 相关的默认视图。我在 Views/Home 文件夹中添加了一个名为 Create.cshtml 的视图文件,并添加了清单26-12所示的标记。

清单 26-12:Views/Home 文件夹下的 Create.cshtml 文件的内容

@model Person
@{
    ViewBag.Title = "Create Person";
    Layout = "_Layout";
}

<form asp-action="Create" method="post">
    <div class="form-group">
        <label asp-for="PersonId"></label>
        <input asp-for="PersonId" class="form-control" />
    </div>
    <div class="form-group">
        <label asp-for="FirstName"></label>
        <input asp-for="FirstName" class="form-control" />
    </div>
    <div class="form-group">
        <label asp-for="LastName"></label>
        <input asp-for="LastName" class="form-control" />
    </div>
    <div class="form-group">
        <label asp-for="Role"></label>
        <select asp-for="Role" class="form-control"
                asp-items="@new SelectList(Enum.GetNames(typeof(Role)))"></select>
    </div>
    <button type="submit" class="btn btn-primary">Submit</button>
</form>

此视图包含一个表单,允许为Person对象的某些属性提供值,并包含一个form元素将数据提交回 Home 控制器中的Create方法的使用HttpPost特性修饰的版本。

接收表单数据的 action 方法使用 /Views/Home/Index.cshtml 视图来显示,您可以通过启动应用程序、导航到 /Home/Create、填写表单并单击【Submit】按钮来查看它是如何工作的,如图26-4所示。

图26-4 使用 CreatePerson action 方法

当表单数据被发送到服务器时,模型绑定过程发现 action 方法需要一个复杂的类型:一个Person对象。Person类被检查以发现它的公共属性。对于每个返回简单属性类型的公共属性,模型绑定程序尝试定位一个请求值,就像在前面的示例中所做的那样。

因此,例如,模型绑定器查找PersonId属性并在上一节中搜索id值的相同位置查找PersonId值。由于表单数据包含一个适当的值,该值是使用input元素上的asp-for标签助手设置的,这是将要使用的值。

如果属性需要另一个复杂类型,则对新类型重复处理。获得一组公共属性,并且绑定器试图为所有属性寻找值。不同之处在于属性名称是嵌套的。例如,Person类的HomeAddress属性是Address类型,如下所示:

using System;

namespace MvcModels.Models {
    public class Person {
        public int PersonId { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public DateTime BirthDate { get; set; }
        public Address HomeAddress { get; set; }
        public bool IsApproved { get; set; }
        public Role Role { get; set; }
    }
    public class Address {
        public string Line1 { get; set; }
        public string Line2 { get; set; }
        public string City { get; set; }
        public string PostalCode { get; set; }
        public string Country { get; set; }
    }
    public enum Role {
        Admin,
        User,
        Guest
    }
}

当查找Line1属性的值时,模型绑定器将查找HomeAddress.Line1的值,如同模型对象中的属性名称与嵌套模型类型中的属性名称相结合。

创建易于绑定的 HTML

前缀的使用意味着视图必须包含模型绑定器查找的信息。这很容易使用标签帮助器来完成,它会自动将所需的前缀添加到它们转换的元素中。在清单26-13中,我扩展了表单以获取地址数据。

清单 26-13:Views/Home 文件夹下的 Create.cshtml 文件,更新表单

@model Person
@{
    ViewBag.Title = "Create Person";
    Layout = "_Layout";
}
<form asp-action="Create" method="post">
    <div class="form-group">
        <label asp-for="PersonId"></label>
        <input asp-for="PersonId" class="form-control" />
    </div>
    <div class="form-group">
        <label asp-for="FirstName"></label>
        <input asp-for="FirstName" class="form-control" />
    </div>
    <div class="form-group">
        <label asp-for="LastName"></label>
        <input asp-for="LastName" class="form-control" />
    </div>
    <div class="form-group">
        <label asp-for="Role"></label>
        <select asp-for="Role" class="form-control"
                asp-items="@new SelectList(Enum.GetNames(typeof(Role)))"></select>
    </div>
    <div class="form-group">
        <label asp-for="HomeAddress.City"></label>
        <input asp-for="HomeAddress.City" class="form-control" />
    </div>
    <div class="form-group">
        <label asp-for="HomeAddress.Country"></label>
        <input asp-for="HomeAddress.Country" class="form-control" />
    </div>
    <button type="submit" class="btn btn-primary">Submit</button>
</form>

在使用标签时,嵌套的属性名是使用 C# 约定指定的,这样外部和嵌套的属性名称就用句点来分隔:HomeAddress.Country。如果运行应用程序,请求 /Home/Create URL,并检查发送到浏览器的 HTML,您将看到某些属性使用了不同的约定。

<div class="form-group">
    <label for="HomeAddress_City">City</label>
    <input class="form-control" type="text" id="HomeAddress_City"
        name="HomeAddress.City" value="" />
</div>
<div class="form-group">
    <label for="HomeAddress_Country">Country</label>
    <input class="form-control" type="text" id="HomeAddress_Country"
        name="HomeAddress.Country" value="" />
</div>

input元素上的name属性遵循 C# 样式,但label元素上的for属性和input元素上的id属性用下划线分隔属性名。如果您喜欢在没有标签助手的情况下定义 HTML 元素,那么应该确保使用相同的命名方案。

由于这个特性,我不需要采取任何特殊措施来确保模型绑定器可以为HomeAddress属性创建Address对象。我可以通过编辑Index.cshtml视图来显示从表单提交的HomeAddress属性,如清单26-14所示。

清单 26-14:Views/Home 文件夹下的 Index.cshtml 文件,显示 HomeAddress 属性

@model Person
@{ Layout = "_Layout"; }

<div class="bg-primary m-1 p-1 text-white"><h2>Person</h2></div>

<table class="table table-sm table-bordered table-striped">
    <tr><th>PersonId:</th><td>@Model.PersonId</td></tr>
    <tr><th>First Name:</th><td>@Model.FirstName</td></tr>
    <tr><th>Last Name:</th><td>@Model.LastName</td></tr>
    <tr><th>Role:</th><td>@Model.Role</td></tr>
    <tr><th>City:</th><td>@Model.HomeAddress?.City</td></tr>
    <tr><th>Country:</th><td>@Model.HomeAddress?.Country</td></tr>
</table>

如果启动应用程序并导航到 /Home/Create URL,则可以输入CityCountry属性的值,并通过提交表单检查它们是否绑定到模型对象,如图26-5所示。

图26-5 复杂对象的属性绑定

指定自定义前缀

在某些情况下,您生成的 HTML 与一种类型的对象相关,但您希望将其绑定到另一种对象。这意味着包含视图的前缀不符合模型绑定器所期望的结构,而且您的数据将无法正确处理。为了演示这个问题,我在 Models 文件夹中添加了一个名为 AddressSummary.cs 的文件,并使用它来定义清单26-15所示的类。

清单 26-15:Models 文件夹下的 AddressSummary.cs 文件的内容

namespace MvcModels.Models
{
    public class AddressSummary
    {
        public string City { get; set; }
        public string Country { get; set; }
    }
}

我在 Home 控制器中使用AddressSummary类添加了一个新的 action 方法,如清单26-16所示。

清单 26-16:Controllers 文件夹下的 HomeController.cs 文件,添加 action 方法

using Microsoft.AspNetCore.Mvc;
using MvcModels.Models;
using System.Linq;

namespace MvcModels.Controllers
{
    public class HomeController : Controller
    {
        private IRepository repository;

        public HomeController(IRepository repo)
        {
            repository = repo;
        }

        public IActionResult Index(int? id)
        {
            Person person;
            if (id.HasValue && (person = repository[id.Value]) != null)
            {
                return View(person);
            }
            else
            {
                return NotFound();
            }
        }

        public ViewResult Create() => View(new Person());

        [HttpPost]
        public ViewResult Create(Person model) => View("Index", model);

        public ViewResult DisplaySummary(AddressSummary summary) => View(summary);
    }
}

新的 action 方法名为DisplaySummary。它有一个AddressSummary参数传递给View方法,以便被默认视图显示。我在 /Views/Home 文件夹中创建了 DisplaySummary.cshtml 文件,并添加了清单26-17所示的标记。

清单 26-17:Views/Home 文件夹下的 DisplaySummary.cshtml 文件的内容

@model AddressSummary
@{
    ViewBag.Title = "DisplaySummary";
    Layout = "_Layout";
}

<div class="bg-primary m-1 p-1 text-white"><h2>Address</h2></div>
<table class="table table-sm table-bordered table-striped">
    <tr><th>City:</th><td>@Model.City</td></tr>
    <tr><th>Country:</th><td>@Model.Country</td></tr>
</table>

此视图显示AddressSummary类定义的两个属性的值。为了演示绑定到不同模型类型时前缀的问题,我更改了 Create.cshtml 视图中的form元素,以便将其数据发送到DisplaySummary action,如清单26-18所示。

清单 26-18:Views/Home 文件夹下的 Create.cshtml 文件,更改表单目标 action

@model Person
@{
    ViewBag.Title = "Create Person";
    Layout = "_Layout";
}
<form asp-action="DisplaySummary" method="post">
    
    <!-- 此处代码省略,按原样,请参照清单26-13 -->

</form>

启动应用程序并导航到 /Home/Create URL,当您提交表单时,可以看到会发生什么情况,您为CityCountry属性输入的值不会显示在DisplaySummary视图生成的 HTML 中。

问题是表单中的name属性具有HomeAddress前缀,这不是模型绑定器试图绑AddressSummary类型时所要寻找的。

要解决这个问题,可以将Bind属性应用于 action 方法参数,该参数指定在模型绑定期间应该使用的前缀,如清单26-19所示。

清单 26-19:Controllers 文件夹下的 HomeController.cs 文件,更改模型绑定前缀

using Microsoft.AspNetCore.Mvc;
using MvcModels.Models;
using System.Linq;

namespace MvcModels.Controllers
{
    public class HomeController : Controller
    {
        private IRepository repository;

        public HomeController(IRepository repo)
        {
            repository = repo;
        }

        public IActionResult Index(int? id)
        {
            Person person;
            if (id.HasValue && (person = repository[id.Value]) != null)
            {
                return View(person);
            }
            else
            {
                return NotFound();
            }
        }

        public ViewResult Create() => View(new Person());

        [HttpPost]
        public ViewResult Create(Person model) => View("Index", model);

        public ViewResult DisplaySummary(
            [Bind(Prefix = nameof(Person.HomeAddress))] AddressSummary summary)
                => View(summary);
    }
}

语法很笨拙,但是效果是有用的。当填充AddressSummary对象的属性时,模型绑定器将在请求中查找HomeAddress.CityHomeAddress.Country数据值。如果您运行应用程序并再次提交表单,将看到您输入到CityCountry字段中的值现在已正确显示,如图26-6所示。对于一个简单的问题来说,这似乎是一个漫长的设置,但是绑定到不同类型的对象的需求是非常普遍的,这是一种值得了解的技术。

图26-6 绑定到不同对象类型的属性

有选择地绑定属性

假设AddressSummary类的Country属性是特别敏感的,并且用户不应能为它指定值。我可以做的第一件事是确保在应用程序的视图中不包含引用该属性的任何 HTML 元素,从而阻止用户查看或编辑该属性。

但是,恶意用户可以在提交表单数据时简单地编辑发送到服务器的表单数据,并为适合他们的Country属性选择值。我真正想做的是告诉模型绑定器不要从请求中绑定Country属性的值,这可以通过在 action 方法参数上配置Bind特性来实现,只指定我想要绑定的属性的名称,如清单26-20所示。

清单 26-20:Controllers 文件夹下的 HomeController.cs 文件,指定属性

using Microsoft.AspNetCore.Mvc;
using MvcModels.Models;
using System.Linq;

namespace MvcModels.Controllers
{
    public class HomeController : Controller
    {
        private IRepository repository;

        public HomeController(IRepository repo)
        {
            repository = repo;
        }

        public IActionResult Index(int? id)
        {
            Person person;
            if (id.HasValue && (person = repository[id.Value]) != null)
            {
                return View(person);
            }
            else
            {
                return NotFound();
            }
        }

        public ViewResult Create() => View(new Person());

        [HttpPost]
        public ViewResult Create(Person model) => View("Index", model);

        public ViewResult DisplaySummary(
            [Bind(nameof(AddressSummary.City), Prefix = nameof(Person.HomeAddress))]
                AddressSummary summary) => View(summary);
    }
}

Bind特性的第一个参数是一个逗号分隔的列表,列出应该包含在模型绑定过程中的属性的名称。在清单中,我指定了City属性应该包括在流程中,并且由于Country没有被列出,这意味它将被排除在外。

如果运行该应用程序,请求 /Home/Create URL,填写并发送表单,您将看到没有为Country属性显示的值,即使浏览器作为 HTTP POST 请求的一部分被发送,如图26-7所示。

图26-7 将属性排除在模型绑定过程中

Bind特性应用于 action 方法参数时,它只影响绑定到该 action 方法的该类的实例;所有其他 action 方法都将继续尝试绑定由参数类型定义的所有属性。如果希望创建更广泛的效果,那么可以将Bind属性应用到模型类本身,如清单26-21所示。

清单 26-21:Models 文件夹下的 AddressSummary.cs 文件,应用绑定特性

using Microsoft.AspNetCore.Mvc;

namespace MvcModels.Models
{
    [Bind(nameof(City))]
    public class AddressSummary
    {
        public string City { get; set; }
        public string Country { get; set; }
    }
}

您还可以通过使用BindNever特性来显式地排除属性,如清单26-22所示,尽管这确实意味着添加到模型类中的新属性将包含在模型绑定过程中,除非您记住将该属性应用于它们。

清单 26-22:Models 文件夹下的 AddressSummary.cs 文件,应用 NeverBind 特性

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;

namespace MvcModels.Models
{
    [Bind(nameof(City))]
    public class AddressSummary
    {
        public string City { get; set; }

        [BindNever]
        public string Country { get; set; }
    }
}

提示:还有一个BindRequired特性,它告诉模型绑定过程一个请求必须包含一个属性的值。如果请求没有一个必需的值,那么就会产生一个模型验证错误,如第27章所述。

绑定至数组和集合

模型绑定过程有一些很好的特性,可以将请求数据绑定到数组和集合,我将在下面的部分中演示这些特性。

绑定至数组

默认模型绑定的一个优雅特性是它如何支持数组的 action 方法参数。为了演示这一点,我向 Home 控制器添加了一个名为Names的新方法,如清单26-23所示。

清单 26-23:Controllers 文件夹下的 HomeController.cs 文件,添加一个 Action 方法

using Microsoft.AspNetCore.Mvc;
using MvcModels.Models;
using System.Linq;

namespace MvcModels.Controllers
{
    public class HomeController : Controller
    {
        private IRepository repository;

        public HomeController(IRepository repo)
        {
            repository = repo;
        }

        public IActionResult Index(int? id)
        {
            Person person;
            if (id.HasValue && (person = repository[id.Value]) != null)
            {
                return View(person);
            }
            else
            {
                return NotFound();
            }
        }

        public ViewResult Create() => View(new Person());

        [HttpPost]
        public ViewResult Create(Person model) => View("Index", model);

        public ViewResult DisplaySummary(
            [Bind(nameof(AddressSummary.City), Prefix = nameof(Person.HomeAddress))]
                AddressSummary summary) => View(summary);

        public ViewResult Names(string[] names) => View(names ?? new string[0]);
    }
}

Names action 方法有一个名为names的字符串数组参数。模型绑定器将查找任何名为names的数据项,并创建包含这些值的数组。为了提供一个视图的 action 方法,我在 Views/Home 文件夹中创建了一个名为 Names.cshtml 的 Razor 文件,并添加了清单26-24所示的标记。

清单 26-24:Views/Home 文件夹下的 Names.cshtml 文件的内容

@model string[]
@{
    ViewBag.Title = "Names";
    Layout = "_Layout";
}

@if (Model.Length == 0)
{
    <form asp-action="Names" method="post">
        @for (int i = 0; i < 3; i++)
        {
            <div class="form-group">
                <label>Name @(i + 1):</label>
                <input id="names" name="names" class="form-control" />
            </div>
        }
        <button type="submit" class="btn btn-primary">Submit</button>
    </form>
}
else
{
    <table class="table table-sm table-bordered table-striped">
        @foreach (string name in Model)
        {
            <tr><th>Name:</th><td>@name</td></tr>
        }
    </table>
    <a asp-action="Names" class="btn btn-primary">Back</a>
}

此视图根据视图模型中的项数显示不同的内容。如果没有项,则视图将显示包含三个相同的input元素的表单,如下所示:

<form method="post" action="/Home/Names">
    <div class="form-group">
        <label>Name 1:</label>
        <input id="names" name="names" class="form-control" />
    </div>
    <div class="form-group">
        <label>Name 2:</label>
        <input id="names" name="names" class="form-control" />
    </div>
    <div class="form-group">
        <label>Name 3:</label>
        <input id="names" name="names" class="form-control" />
    </div>
    <button type="submit" class="btn btn-primary">Submit</button>
</form>

当表单提交时,模型绑定处理将看到目标 action 方法接受一个数组,并查找与 action 方法参数具有相同名称的数据项。对于本例,这意味着来自input元素(其name属性为names)的所有值将被聚集在一起创建一个数组,并用作参数来调用 action 方法。要查看效果,启动应用程序,导航到 /Home/Names URL,并填写表单。当您提交表单时,将看到您输入的所有值都显示出来,如图26-8所示。

图26-8 数组的模型绑定

绑定至集合

模型绑定过程可以创建的不仅仅是数组,它还支持集合类。在清单26-25中,我将Names action 方法参数的类型更改为强类型列表。

清单 26-25:Controllers 文件夹下的 HomeController.cs 文件,使用强类型集合

using Microsoft.AspNetCore.Mvc;
using MvcModels.Models;
using System.Collections.Generic;

namespace MvcModels.Controllers
{
    public class HomeController : Controller
    {
        private IRepository repository;

        public HomeController(IRepository repo)
        {
            repository = repo;
        }

        //其它 action 方法省略,请参照清单26-23

        public ViewResult Names(IList<string> names) =>
            View(names ?? new List<string>());
    }
}

我使用了IList<T>接口,不需要指定具体的实现类,不过如果愿意的话,可以使用。在清单26-26中,我修改了 Names.cshtml 视图文件以使用新的模型类型。

清单 26-26:Views/Home 文件夹下的 Names.cshtml 文件,使用集合作为模型类型

@model IList<string>
@{
    ViewBag.Title = "Names";
    Layout = "_Layout";
}

@if (Model.Count == 0)
{
    <form asp-action="Names" method="post">
        @for (int i = 0; i < 3; i++)
        {
            <div class="form-group">
                <label>Name @(i + 1):</label>
                <input id="names" name="names" class="form-control" />
            </div>
        }
        <button type="submit" class="btn btn-primary">Submit</button>
    </form>
}
else
{
    <table class="table table-sm table-bordered table-striped">
        @foreach (string name in Model)
        {
            <tr><th>Name:</th><td>@name</td></tr>
        }
    </table>
    <a asp-action="Names" class="btn btn-primary">Back</a>
}

Names action 的功能不变,但我现在可以使用集合类而不是数组。

绑定至复杂类型的集合

您还可以将单个数据值绑定到一个复杂类型的数组中,该数组允许从单个请求中收集多个对象(例如示例中的AddressSummary模型类)。在清单26-27中,我向 Home 控制器添加了一个名为Address的 action 方法,其参数是一个AddressSummary对象列表。

清单 26-27:Controllers 文件夹下的 HomeController.cs 文件,定义一个 Action 方法

using Microsoft.AspNetCore.Mvc;
using MvcModels.Models;
using System.Collections.Generic;

namespace MvcModels.Controllers
{
    public class HomeController : Controller
    {
        private IRepository repository;

        public HomeController(IRepository repo)
        {
            repository = repo;
        }

        public IActionResult Index(int? id)
        {
            Person person;
            if (id.HasValue && (person = repository[id.Value]) != null)
            {
                return View(person);
            }
            else
            {
                return NotFound();
            }
        }

        public ViewResult Create() => View(new Person());

        [HttpPost]
        public ViewResult Create(Person model) => View("Index", model);

        public ViewResult DisplaySummary(
            [Bind(nameof(AddressSummary.City), Prefix = nameof(Person.HomeAddress))]
                AddressSummary summary) => View(summary);

        public ViewResult Names(IList<string> names) =>
            View(names ?? new List<string>());

        public ViewResult Address(IList<AddressSummary> addresses) =>
            View(addresses ?? new List<AddressSummary>());
    }
}

为了提供带有视图的新 action 方法,我向 Views/Home 文件夹中添加了一个名为 Address.cshtml 的文件,并添加了清单26-28所示的标记。

清单 26-28:Views/Home 文件夹下的 Address.cshtml 文件的内容

@model IList<AddressSummary>
@{
    ViewBag.Title = "Address";
    Layout = "_Layout";
}

@if (Model.Count() == 0)
{
    <form asp-action="Address" method="post">
        @for (int i = 0; i < 3; i++)
        {
            <fieldset class="form-group">
                <legend>Address @(i + 1)</legend>
                <div class="form-group">
                    <label>City:</label>
                    <input name="[@i].City" class="form-control" />
                </div>
                <div class="form-group">
                    <label>Country:</label>
                    <input name="[@i].Country" class="form-control" />
                </div>
            </fieldset>
        }
        <button type="submit" class="btn btn-primary">Submit</button>
    </form>
}
else
{
    <table class="table table-sm table-bordered table-striped">
        <tr><th>City</th><th>Country</th></tr>
        @foreach (var address in Model)
        {
            <tr><td>@address.City</td><td>@address.Country</td></tr>
        }
    </table>
    <a asp-action="Address" class="btn btn-primary">Back</a>
}

如果模型集合中没有项,此视图将渲染一个form元素。该form由一对input元素组成,其name属性以数组索引作为前缀,如下所示:

...
<form method="post" action="/Home/Address">
    <fieldset class="form-group">
        <legend>Address 1</legend>
        <div class="form-group">
            <label>City:</label>
            <input name="[0].City" class="form-control" />
        </div>
        <div class="form-group">
            <label>Country:</label>
            <input name="[0].Country" class="form-control" />
        </div>
    </fieldset>
    <fieldset class="form-group">
        <legend>Address 2</legend>
        <div class="form-group">
            <label>City:</label>
            <input name="[1].City" class="form-control" />
        </div>
        <div class="form-group">
            <label>Country:</label>
            <input name="[1].Country" class="form-control" />
        </div>
    </fieldset>
    <fieldset class="form-group">
        <legend>Address 3</legend>
        <div class="form-group">
            <label>City:</label>
            <input name="[2].City" class="form-control" />
        </div>
        <div class="form-group">
            <label>Country:</label>
            <input name="[2].Country" class="form-control" />
        </div>
    </fieldset>
    <button type="submit" class="btn btn-primary">Submit</button>
</form>
...

当表单提交时,模型绑定器意识到它需要创建一个AddressSummary对象的集合,并使用name属性中的数组索引前缀来获取对象属性的值。前缀为[0]的属性用于第一个AddressSummary对象,前缀为[1]的属性用于第二个对象,依此类推。

Address.cshtml 视图定义了三个这样的索引对象的input元素,并在模型集合包含项目时显示它们。在我演示这一点之前,我需要从AddressSummary模型类中删除BindNever特性,如清单26-29所示;否则,模型绑定器将忽略Country属性。

清单 26-29:Models 文件夹下的 AddressSummary.cs 文件,移除 BindNever 特性

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;

namespace MvcModels.Models
{
    [Bind(nameof(City))]
    public class AddressSummary
    {
        public string City { get; set; }

        //[BindNever]
        public string Country { get; set; }
    }
}

通过启动应用程序并导航到 /Home/Address URL,您可以看到自定义对象集合的绑定过程是如何工作的。输入一些城市和国家,然后单击提交按钮将表单提交到服务器。

模型绑定过程将查找和处理索引数据值,并使用它们创建提供给 action 方法的AddressSummary对象集合,然后使用View便捷方法将它们传递回视图,以进行显示,如图26-9所示。

图26-9 绑定自定义对象集合

指定模型绑定源

正如我在本章开头解释的那样,默认模型绑定过程在三个位置查找数据:表单的数据值、路由数据和请求查询字符串。

默认的搜索序列并不总是有用的,要么是因为您总是希望数据来自请求的特定部分,要么是因为您希望使用默认情况下不被搜索的数据源。模型绑定特性包括一组用于覆盖默认搜索行为的属性,如表26-3所述。

表 26-3:模型绑定源属性

名称 描述
FromForm 此属性用于选择表单数据作为绑定数据的源。默认情况下,参数的名称用于定位表单值,但可以使用Name属性进行更改,该属性允许指定不同的名称。
FromRoute 此属性用于选择路由系统作为绑定数据源。默认情况下,参数的名称用于定位路由数据值,但可以使用Name属性进行更改,该属性允许指定不同的名称。
FromQuery 此属性用于选择查询字符串作为绑定数据的来源。默认情况下,参数的名称用于定位查询字符串值,但可以使用Name属性进行更改,该属性允许指定不同的查询字符串键。
FromHeader 此属性用于选择请求 header 作为绑定数据的源。默认情况下,参数的名称用作 header 名称,但可以使用Name属性进行更改,该属性允许指定不同的 header 名称。
FromBody 此属性用于指定请求 body 应作为绑定数据的来源,当您希望从非表单编码的请求(如 API 控制器中)接收数据时,这是必需的。

选择标准绑定源

FromFormFromRouteFromQuery属性允许您指定模型绑定数据将从一个标准位置获得,而不需要正常的搜索顺序。在本章前面的部分中,我使用了这个 URL:

/Home/Index/3?id=1

此 URL 包含两个可能的值,可用于 Home 控制器上Index action 方法的id参数。路由系统将 URL 的最后部分分配给一个名为id的变量,该变量在Startup类的 URL 模式中定义,查询字符串还包含一个id值。默认搜索模式意味着模型绑定数据将从路由数据中提取,查询字符串将被忽略。

为了改变这种行为,在清单26-30中,我将FromQuery属性应用于 action 方法。为了保持示例简单,我还删除了前面示例中定义的所有其他 action 方法。

清单 26-30:Controllers 文件夹下的 HomeController.cs 文件,选择查询字符串

using Microsoft.AspNetCore.Mvc;
using MvcModels.Models;
namespace MvcModels.Controllers
{
    public class HomeController : Controller
    {
        private IRepository repository;
        public HomeController(IRepository repo)
        {
            repository = repo;
        }

        public IActionResult Index([FromQuery] int? id)
        {
            Person person;
            if (id.HasValue && (person = repository[id.Value]) != null)
            {
                return View(person);
            }
            else
            {
                return NotFound();
            }
        }
    }
}

我已经将FromQuery属性应用于id参数,这意味着在模型绑定过程寻找id数据值时只使用查询字符串。

提示:在指定模型绑定源(如查询字符串)时,仍然可以绑定复杂类型。对于参数类型中的每个简单属性,模型绑定过程将查找具有相同名称的查询字符串键。

将 Headers 作为绑定源

FromHeader特性允许将 HTTP 请求 header 用作绑定数据的源。在清单26-31中,我向 Home 控制器添加了一个简单的 action 方法,该方法使用来自标准 HTTP 请求 header 的数据接收绑定的参数。

清单 26-31:Controllers 文件夹下的 HomeController.cs 文件,来自 Header 的模型绑定

using Microsoft.AspNetCore.Mvc;
using MvcModels.Models;
namespace MvcModels.Controllers
{
    public class HomeController : Controller
    {
        private IRepository repository;
        public HomeController(IRepository repo)
        {
            repository = repo;
        }

        public IActionResult Index([FromQuery] int? id)
        {
            Person person;
            if (id.HasValue && (person = repository[id.Value]) != null)
            {
                return View(person);
            }
            else
            {
                return NotFound();
            }
        }

        public string Header([FromHeader]string accept) => $"Header: {accept}";
    }
}

Header action 方法定义一个accept参数,该参数的值将从当前请求中的Accept header 中获取,并作为方法结果返回。如果运行应用程序并请求 /Home/Header URL,您将看到这样的结果(尽管使用不同的浏览器,确切的结果可能有所不同):

Header: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8

并不是所有的 HTTP header 名称都可以通过依赖 action 方法参数的名称来轻松选择,因为模型绑定系统不会将 C# 命名约定转换为 HTTP header 使用的命名约定。在这种情况下,必须使用Name属性配置FromHeader属性以指定 header 的名称,如清单26-32所示。

清单 26-32:Controllers 文件夹下的 HomeController.cs 文件,指定标头的名称

using Microsoft.AspNetCore.Mvc;
using MvcModels.Models;

namespace MvcModels.Controllers
{
    public class HomeController : Controller
    {
        private IRepository repository;
        public HomeController(IRepository repo)
        {
            repository = repo;
        }

        public IActionResult Index([FromQuery] int? id)
        {
            Person person;
            if (id.HasValue && (person = repository[id.Value]) != null)
            {
                return View(person);
            }
            else
            {
                return NotFound();
            }
        }

        public string Header([FromHeader(Name = "Accept-Language")] string accept)
            => $"Header: {accept}";
    }
}

我不能使用Accept-Language作为 C# 参数的名称,而且模型绑定器不会自动将像AcceptLanguage这样的名称转换为Accept-Language,以便与 header 匹配。如果启动应用程序并请求 /Home/Header URL,您将看到类似于此的响应,它将根据您的地区设置而有所不同:

Header: en-US,en;q=0.8

通过标头绑定复杂类型

虽然这是一个罕见的要求,但您可以通过将FromHeader特性应用于模型类的属性来使用 header 值绑定复杂类型。例如,我在 Models 文件夹中添加了一个名为 HeaderModel.cs 的文件,并定义了清单26-33中所示的类。

清单 26-33:Models 文件夹下的 HeaderModel.cs 文件的内容

using Microsoft.AspNetCore.Mvc;

namespace MvcModels.Models
{
    public class HeaderModel
    {
        [FromHeader]
        public string Accept { get; set; }

        [FromHeader(Name = "Accept-Language")]
        public string AcceptLanguage { get; set; }

        [FromHeader(Name = "Accept-Encoding")]
        public string AcceptEncoding { get; set; }
    }
}

该类定义了三个属性,每个属性都使用FromHeader特性进行修饰。我使用了两个特性的Name属性来指定 header 名称不能用 C# 参数名来表示。在清单26-34中,我更新了 Home 控制器中的 Header action 方法,以接收HeaderModel对象。

清单 26-34:Controllers 文件夹下的 HomeController.cs 文件,使用 Header 模型类

using Microsoft.AspNetCore.Mvc;
using MvcModels.Models;

namespace MvcModels.Controllers
{
    public class HomeController : Controller
    {
        private IRepository repository;
        public HomeController(IRepository repo)
        {
            repository = repo;
        }

        public IActionResult Index([FromQuery] int? id)
        {
            Person person;
            if (id.HasValue && (person = repository[id.Value]) != null)
            {
                return View(person);
            }
            else
            {
                return NotFound();
            }
        }

        public ViewResult Header(HeaderModel model) => View(model);
    }
}

为了完成这个示例,我在 Views/Home 文件夹中添加了一个名为 Header.cshtml 的视图文件,并添加了清单26-35中所示的标记。

清单 26-35:Views/Home 文件夹下的 Header.cshtml 文件的内容

@model HeaderModel
@{
    ViewBag.Title = "Headers";
    Layout = "_Layout";
}

<table class="table table-sm table-bordered table-striped">
    <tr><th>Accept:</th><td>@Model.Accept</td></tr>
    <tr><th>Accept-Encoding:</th><td>@Model.AcceptEncoding</td></tr>
    <tr><th>Accept-Language:</th><td>@Model.AcceptLanguage</td></tr>
</table>

模型绑定过程将检查复杂类型的属性,查找表26-3中描述的属性。这允许我使用FromHeader特性定义一个复杂类型,其属性是从 header 绑定的,如果您运行应用程序并请求 /Home/Header URL,可以看到它产生了如图26-10所示的结果。

图26-10 来自请求 header 的复杂类型的模型绑定

使用请求 body 作为绑定源

并非所有由客户端发送的数据都以表单数据的形式发送,例如当 JavaScript 客户端将 JSON 数据发送到 API 控制器时。FromBody特性指定请求 body 应该被解码并用作模型绑定数据的来源。在清单26-36中,我添加了新的Body action 方法,演示了它是如何工作的。

清单 26-36:Controllers 文件夹下的 HomeController.cs 文件,添加 Action 方法

using Microsoft.AspNetCore.Mvc;
using MvcModels.Models;

namespace MvcModels.Controllers
{
    public class HomeController : Controller
    {
        private IRepository repository;
        public HomeController(IRepository repo)
        {
            repository = repo;
        }

        public IActionResult Index([FromQuery] int? id)
        {
            Person person;
            if (id.HasValue && (person = repository[id.Value]) != null)
            {
                return View(person);
            }
            else
            {
                return NotFound();
            }
        }

        public ViewResult Header(HeaderModel model) => View(model);

        public ViewResult Body() => View();

        [HttpPost]
        public Person Body([FromBody]Person model) => model;
    }
}

我用FromBody特性修饰了接受POST请求的Body方法的参数,这意味着请求正文内容将被解码并用于模型绑定。正如我在第20章中解释的那样,MVC 有一个用于处理数据格式的可扩展系统,但默认情况下,它只用于处理 JSON 数据。

接下来,我编辑了 libman.json 文件以将 jQuery 添加到应用程序中,如清单26-37所示

清单 26-37:MvcModels 文件夹下的 bower.json 文件,添加 jQuery

{
  "version": "1.0",
  "defaultProvider": "cdnjs",
  "libraries": [
    {
      "library": "twitter-bootstrap@4.1.3",
      "destination": "wwwroot/lib/twitter-bootstrap/"
    },
    {
      "library": "jquery@3.3.1",
      "destination": "wwwroot/lib/jquery/"
    }
  ]
}

为了向 action 方法提供它所需的数据,我将一个名为 Body.cshtml 的文件添加到 Views/Home 文件夹中,并添加了清单 26-38 所示的内容。

清单 26-38:Views/Home 文件夹下的 Body.cshtml 文件的内容

@{
    ViewBag.Title = "Address";
    Layout = "_Layout";
}

@section scripts {
    <script src="/lib/jquery/dist/jquery.min.js"></script>
    <script type="text/javascript">
        $(document).ready(function () {
            $("button").click(function (e) {
                $.ajax("/Home/Body", {
                    method: "post",
                    contentType: "application/json",
                    data: JSON.stringify({
                        firstName: "Bob",
                        lastName: "Smith"
                    }),
                    success: function (data) {
                        $("#firstName").text(data.firstName);
                        $("#lastName").text(data.lastName);
                    }
                });
            });
        });
    </script>
}

<table class="table table-sm table-bordered table-striped">
    <tr><th>First Name:</th><td id="firstName"></td></tr>
    <tr><th>Last Name:</th><td id="lastName"></td></tr>
</table>
<button class="btn btn-primary">Submit</button>

为了简单起见,该视图包含一些内联 JavaScript 代码,当单击button元素时,它使用 jQuery 向 /Home/Body URL 发送包含 JSON 数据的 HTTP POST 请求。服务器对使用模型绑定创建的对象进行编码,并将其发送回客户机,编码为 JSON。通过运行应用程序、请求 /Home/Body URL 并单击【Submit】按钮,您可以看到效果,如图26-11所示。

图26-11 使用请求 body 进行模型绑定

提示:并非所有的 JavaScript 客户端代码都需要使用FromBody特性。在本例中,我不得不避免使用 jQuery 便捷方法发送 Ajax POST 请求,因为它将数据编码为表单数据。因此必须使用不同方法发送 JSON 数据。

FromBody特性只能用于模型绑定一个 action 方法参数,如果该特性对单个方法多次使用,则会引发异常。如果需要从请求 body 创建多个模型对象,则必须创建具有所需所有属性的简单数据传输类,并使用它包含的数据来创建 action 方法中所需的对象。

总结

在本章中,我描述了模型绑定过程,它使用正在处理的 HTTP 请求中的数据值为 action 方法提供它们所需的参数。我解释了模型如何绑定简单和复杂类型,如何处理数组和集合,以及如何通过将属性应用于 action 方法参数或模型类属性来控制模型绑定过程。在下一章中,我将描述模型验证特性。

;

© 2018 - IOT小分队文章发布系统 v0.3